1 /** 2 The keyedcollection module contains: 3 $(TOC Enforce) 4 $(TOC usableForKeyedCollection) 5 $(TOC BaseKeyedCollection) 6 $(TOC KeyedCollection) 7 8 License: $(GPL2) 9 10 Authors: Matthew Armbruster 11 12 $(B Source:) $(SRC $(SRCFILENAME)) 13 14 Copyright: 2016 15 */ 16 module db_constraints.keyed.keyedcollection; 17 18 import std.algorithm : canFind, endsWith, each; 19 import std.conv : to; 20 import std.exception : enforceEx; 21 import std.traits; 22 import std.typecons : Flag, Yes, No; 23 24 import db_constraints.db_exceptions; 25 import db_constraints.keyed.keyeditem; 26 import db_constraints.utils.meta; 27 28 /** 29 Tells the keyed collection which constraints to check. 30 */ 31 enum Enforce 32 { 33 /** 34 Set $(SRCTAG KeyedCollection.enforceConstraints) equal to this if you do 35 not want any constraints to be enforced. 36 */ 37 none = 0, 38 /** 39 Enforce the item's check constraint meaning anything with 40 $(WIKI constraints, NotNull) or $(WIKI constraints, CheckConstraint). 41 42 Not using this means an item will not be checked when it is added 43 to the collection. If you set up the singular class like the examples 44 though the setter method will still check constraints. 45 */ 46 check = 1 << 0, 47 /** 48 Enforce the collection does not already contain 49 the item you are trying to add. Makes sure there would not 50 be conflicting clustered indicies. 51 */ 52 clusteredUnique = 1 << 1, 53 /** 54 Enforce all unique constraints are not being violated. If 55 you have this then you do not need to have clusteredUnique. 56 */ 57 unique = 1 << 2, 58 /** 59 Enforce the foreign key constraints if there are any. 60 */ 61 foreignKey = 1 << 3, 62 /** 63 Enforce the exclusion constraints if there are any. 64 65 Version: \>= 0.0.7 66 */ 67 exclusion = 1 << 4 68 } 69 70 /** 71 Makes sure the class is usable for keyed collection. This really just 72 makes sure it has the necessary members that come with keyeditem. 73 Returns: 74 true if class can be used for keyed collection 75 */ 76 template usableForKeyedCollection(alias T) 77 { 78 enum usableForKeyedCollection = ( is(T == class) && 79 __traits(compiles, 80 (T t, T i) 81 { 82 if (i.key == t.key) { } 83 class Example 84 { 85 void itemChanged(string s, typeof(T.key) k) { } 86 void add(T item) 87 { 88 item.emitChange.connect(&itemChanged); 89 } 90 } 91 t.checkConstraints(); 92 t.markAsSaved(); 93 auto j = new Example(); 94 j.add(t); 95 string k = t.toString; 96 })); 97 } 98 99 /** 100 Turns the inheriting class into a base keyed collection. 101 The key is based on the singular class' clustered index. 102 The requirements are taken care of when 103 you include the keyeditem in the $(I T) class. 104 If you plan on changing the singular class' clustered index, 105 you must define $(D dup()) that returns a new instance of your class. 106 107 If $(D T) has foreign keys you must use $(SRCTAG KeyedCollection) instead 108 since the functions that come with foreign keys need to have the 109 other class imported. 110 111 This also allows you to make a keyed collection in one line. 112 $(D_CODE alias Candies = BaseKeyedCollection!(Candy);) 113 Now you can use Candies as a collection. 114 Params: 115 T = the singular class 116 */ 117 class BaseKeyedCollection(T) 118 if (usableForKeyedCollection!(T)) 119 { 120 mixin KeyedCollection!(T); 121 } 122 123 124 /** 125 Turns the inheriting class into a keyed collection. 126 The key is based on the singular class' clustered index. 127 The requirements (except for dup) are taken care of when 128 you include the keyeditem in the $(I T) class. 129 130 $(D T) should represent a single row in the database. Use 131 this when $(D T) has foreign keys. 132 */ 133 mixin template KeyedCollection(T) 134 if (usableForKeyedCollection!(T)) 135 { 136 import std.algorithm : canFind, endsWith, each, filter; 137 import std.signals; 138 import std.traits : isIterable; 139 140 141 /** 142 The $(D key_type) is alias'd as the type since it looked better than having 143 $(D typeof(T.key)) everywhere. 144 */ 145 final alias key_type = typeof(T.key); 146 /** 147 Alias letting you know what this is a collection of. 148 149 Version: \>= 0.0.6 150 */ 151 final alias collectionof = T; 152 153 private bool _containsChanges; 154 private ubyte _enforceConstraints = (Enforce.check | 155 Enforce.unique | 156 Enforce.foreignKey | 157 Enforce.exclusion); 158 159 static if (hasForeignKeys!(T)) 160 { 161 mixin(createForeignKeyProperties!(T)); 162 /** 163 Called when you associate a foreign key or an item changed. This checks 164 the current items against its foreign keyed class. 165 */ 166 final private void checkForeignKeys() 167 { 168 this.byValue.each!( 169 (T a) => 170 { 171 mixin(createForeignKeyCheckExceptions!(T)); 172 }()); 173 } 174 /// ditto 175 final private void checkForeignKeys(T a) 176 { 177 mixin(createForeignKeyCheckExceptions!(T)); 178 } 179 mixin(createForeignKeyChanged!(T)); 180 } 181 /** 182 Called when an item is being added or an item changed. This checks 183 the item's check constraints, unique constraints, and foreign key constraints. 184 */ 185 final private void checkConstraints(T item) 186 { 187 if (_enforceConstraints & Enforce.check) 188 { 189 item.checkConstraints(); 190 } 191 if (_enforceConstraints & Enforce.clusteredUnique) 192 { 193 auto i = (item in this); 194 enforceEx!UniqueConstraintException( 195 (i is null || (*i) is item), 196 "The " ~ key_type.stringof ~ " constraint for class " ~ 197 T.stringof ~ 198 " was violated by item " ~ item.toString ~ "."); 199 } 200 if (_enforceConstraints & Enforce.unique) 201 { 202 auto constraintName = ""; 203 enforceEx!UniqueConstraintException( 204 !violatesUniqueConstraints(item, constraintName), 205 "The " ~ constraintName ~ " constraint for class " ~ 206 T.stringof ~ 207 " was violated by item " ~ item.toString ~ "."); 208 } 209 if (_enforceConstraints & Enforce.foreignKey) 210 { 211 static if (hasForeignKeys!(T)) 212 { 213 checkForeignKeys(item); 214 } 215 } 216 if (_enforceConstraints & Enforce.exclusion) 217 { 218 auto constraintName = ""; 219 enforceEx!ExclusionConstraintException( 220 !violatesExclusionConstraints(item, constraintName), 221 "The " ~ constraintName ~ " constraint for class " ~ 222 T.stringof ~ 223 " was violated by item " ~ item.toString ~ "."); 224 } 225 } 226 /// ditto 227 final private void checkConstraints(key_type item_key) 228 { 229 checkConstraints(this[item_key]); 230 } 231 /** 232 $(D itemChanged) is connected to the signal emitted by the item. This checks 233 constraints and makes sure the changes are acceptable. 234 235 $(THROWS KeyedException, if $(D dup()) is not defined and you change the 236 clustered index.) 237 */ 238 final private void itemChanged(string propertyName, key_type item_key) 239 { 240 key_type emit_key = item_key; 241 if (propertyName == "key") 242 { 243 static if ( __traits(compiles, 244 (T t) 245 { 246 T i = t.dup(); 247 })) 248 { 249 T item = this._items[item_key].dup(); 250 this.remove(item_key, No.notifyChange); 251 this.add(item, No.notifyChange); 252 emit_key = item.key; 253 } 254 else 255 { 256 enum msg = T.stringof ~ " is trying to change its clustered " ~ 257 "index without defining a dup() function."; 258 throw new KeyedException(msg); 259 } 260 } 261 else if (propertyName.endsWith("_key")) 262 { 263 checkConstraints(item_key); 264 } 265 notify(propertyName, emit_key); 266 } 267 T[key_type] _items; 268 /** 269 The signal used to emit changes that occur in $(D this). 270 */ 271 mixin Signal!(string, key_type) collectionChanged; 272 /** 273 Changes $(D this) to not contain changes and also marks all 274 the items as saved. Should only be used after a save. 275 */ 276 final void markAsSaved() nothrow pure @nogc 277 { 278 _containsChanges = false; 279 this.byValue.each!(a => a.markAsSaved()); 280 } 281 /** 282 Read-only property telling if $(D this) contains changes. 283 Returns: 284 true if $(D this) contains changes. 285 */ 286 final @property bool containsChanges() const nothrow pure @safe @nogc 287 { 288 return _containsChanges; 289 } 290 /** 291 Write-only property to enforce the constraints. By default 292 this is $(D (Enforce.check | Enforce.unique | Enforce.foreignKey | Enforce.exclusion)) 293 but you may set it to 0 if you have a lot of 294 initial data and already trust that it does not violate any constraints. 295 296 Setting this to false means that there are no checks and if there 297 is a duplicate clustered index, it will be overwritten. 298 */ 299 final @property void enforceConstraints(ubyte value) nothrow pure @safe @nogc 300 { 301 _enforceConstraints = value; 302 } 303 304 /** 305 Notifies $(D this) which property changed and sets containsChanges to true. 306 This also emits a signal with the property name that changed 307 and the key to it in this collection. 308 Params: 309 propertyName = the property name that changed 310 item_key = the items key that changed 311 */ 312 final void notify()(string propertyName, key_type item_key) 313 { 314 _containsChanges = true; 315 collectionChanged.emit(propertyName, item_key); 316 } 317 /** 318 Removes an item from $(D this) and disconnects the signals. Notifies 319 that the length of $(D this) has changed by emitting "remove". 320 */ 321 final void remove(key_type item_key, Flag!"notifyChange" notifyChange = Yes.notifyChange) 322 { 323 if (this.contains(item_key)) 324 { 325 this._items[item_key].disconnect(&itemChanged); 326 this._items.remove(item_key); 327 if (notifyChange) 328 { 329 notify("remove", item_key); 330 } 331 } 332 } 333 /// ditto 334 final void remove(T item) 335 in 336 { 337 assert(item !is null, "Trying to remove a null item."); 338 } 339 body 340 { 341 this.remove(item.key); 342 } 343 /// ditto 344 final void remove(A...)(A a) 345 in 346 { 347 static assert(A.length == key_type.tupleof.length, T.stringof ~ 348 " has a clustered index with " ~ 349 key_type.tupleof.length.to!string ~ 350 " member(s). You included " ~ A.length.to!string ~ 351 " members when using remove."); 352 } 353 body 354 { 355 auto clIdx = key_type(a); 356 return this.remove(clIdx); 357 } 358 /** 359 Adds $(D item) to $(D this) and connects to the signals emitted by $(D item). 360 Notifies that the length of $(D this) has changed. 361 362 $(THROWS UniqueConstraintException, if $(D this) already contains $(D item) and 363 enforceConstraints include $(SRCTAG Enforce.unique) or 364 $(SRCTAG Enforce.clusteredUnique).) 365 366 $(THROWS CheckConstraintException, if the item is violating any of its 367 defined check constraints and enforceConstraints include 368 $(SRCTAG Enforce.check).) 369 370 $(THROWS ForeignKeyException, if the item is violating any of its 371 foreign key constraints and enforceConstraints include 372 $(SRCTAG Enforce.foreignKey).) 373 374 $(THROWS ExclusionConstraintException, if $(D item) conflicts with any item 375 in $(D this) via the ExclusionConstraint and enforceConstraint includes 376 $(SRCTAG Enforce.exclusion).) 377 378 $(B Precondition:) $(D_CODE assert(item(s) !is null);) 379 380 Params: 381 item(s) = the item(s) you want to add to $(D this) 382 notifyChange = whether or not to emit this change. Should only be No if coming from itemChanged 383 */ 384 final void add(T item, Flag!"notifyChange" notifyChange = Yes.notifyChange) 385 in 386 { 387 assert(item !is null, "Trying to add a null item."); 388 } 389 body 390 { 391 this.checkConstraints(item); 392 item.emitChange.connect(&itemChanged); 393 this._items[item.key] = item; 394 if (notifyChange) 395 { 396 notify("add", item.key); 397 } 398 } 399 /// ditto 400 final void add(I)(I items, Flag!"notifyChange" notifyChange = Yes.notifyChange) 401 if (isIterable!(I)) 402 in 403 { 404 assert(items !is null, "Trying to add a null array"); 405 } 406 body 407 { 408 foreach(item; items) 409 { 410 assert(is(typeof(item) == T)); 411 this.add(item, notifyChange); 412 } 413 } 414 /** 415 This just calls $(SRCTAG KeyedCollection.add). 416 */ 417 final ref auto opOpAssign(string op : "~")(T item) 418 { 419 this.add(item); 420 } 421 /// ditto 422 final ref auto opOpAssign(string op : "~", I)(I items) 423 if (isIterable!(I)) 424 { 425 this.add(items); 426 } 427 428 /** 429 Initializes $(D this). Adds $(D item) to $(D this) and connects to the signals 430 emitted by $(D item). 431 432 This just calls $(SRCTAG KeyedCollection.add). 433 434 $(B Precondition:) $(D_CODE assert(item(s) !is null);) 435 */ 436 final this(T item) 437 in 438 { 439 assert(item !is null, "Trying to initialize with a null " ~ T.stringof ~ "."); 440 } 441 body 442 { 443 this.add(item, No.notifyChange); 444 } 445 /// ditto 446 final this(I)(I items) 447 if (isIterable!(I)) 448 in 449 { 450 assert(items !is null, "Trying to initialize with a null iterable."); 451 } 452 body 453 { 454 this.add(items, No.notifyChange); 455 } 456 /// ditto 457 final this() 458 { 459 } 460 461 462 /** 463 Gets the approriate $(D T). You can either use an item 464 that equals the item you want back, a key of the item you want 465 back or parameters that can make the key for the item you want back. 466 Returns: 467 The item in the collection that matches $(D item). 468 469 $(THROWS KeyedException, if $(D this) does not contain a matching 470 clustered index.) 471 472 $(B Precondition:) $(D_CODE assert(item !is null);) 473 */ 474 final ref inout(T) opIndex(in T item) inout 475 in 476 { 477 assert(item !is null, "Trying to lookup with a null."); 478 } 479 body 480 { 481 return this[item.key]; 482 } 483 /// ditto 484 final ref inout(T) opIndex(in key_type clIdx) inout 485 { 486 if (this.contains(clIdx)) 487 { 488 return this._items[clIdx]; 489 } 490 else 491 { 492 auto fields = "\nAn item with clustered index of:\n"; 493 foreach(i, j; clIdx.tupleof) 494 { 495 fields ~= clIdx.tupleof[i].stringof ~ " = " ~ j.to!string() ~ "\n"; 496 } 497 fields ~= "does not exist in " ~ typeof(this).stringof; 498 throw new KeyedException(fields); 499 } 500 } 501 /// ditto 502 final ref inout(T) opIndex(A...)(in A a) inout 503 in 504 { 505 static assert(A.length == key_type.tupleof.length, T.stringof ~ 506 " has a clustered index with " ~ 507 key_type.tupleof.length.to!string ~ 508 " member(s). You included " ~ A.length.to!string ~ 509 " members when using the index."); 510 } 511 body 512 { 513 auto clIdx = key_type(a); 514 return this[clIdx]; 515 } 516 /** 517 Forwards all methods not specified by this abstract class 518 to the private associative array. 519 */ 520 auto opDispatch(string name, A...)(A a) 521 { 522 debug(dispatch) pragma(msg, "opDispatch", name); 523 return mixin("this._items." ~ name ~ "(a)"); 524 } 525 /** 526 Allows you to use $(D this) in a foreach loop. 527 */ 528 final int opApply(int delegate(ref T) dg) 529 { 530 int result = 0; 531 foreach(T i; this.values) 532 { 533 result = dg(i); 534 if (result) 535 break; 536 } 537 return result; 538 } 539 /// ditto 540 final int opApply(int delegate(key_type, ref T) dg) 541 { 542 int result = 0; 543 foreach(T i; this.values) 544 { 545 result = dg(i.key, i); 546 if (result) 547 break; 548 } 549 return result; 550 } 551 /** 552 Gets the length of the collection. 553 Returns: 554 The number of items in the collection. 555 */ 556 final size_t length() const @property @safe nothrow pure 557 { 558 return this._items.length; 559 } 560 /** 561 Checks if $(D item) is in the collection. 562 Params: 563 item = the item you want to see is in the collection 564 Returns: 565 true if $(D item) is in the collection. 566 */ 567 final bool contains(in T item) const nothrow pure @safe @nogc 568 { 569 return this.contains(item.key); 570 } 571 /// ditto 572 final bool contains(in key_type clIdx) const nothrow pure @safe @nogc 573 { 574 auto i = (clIdx in this._items); 575 return (i !is null); 576 } 577 /// ditto 578 final bool contains(A...)(in A a) const nothrow pure @safe @nogc 579 in 580 { 581 static assert(A.length == key_type.tupleof.length, T.stringof ~ 582 " has a clustered index with " ~ 583 key_type.tupleof.length.to!string ~ 584 " member(s). You included " ~ A.length.to!string ~ 585 " members when using contains."); 586 } 587 body 588 { 589 auto clIdx = key_type(a); 590 return this.contains(clIdx); 591 } 592 /** 593 The $(WEB dlang.org/expression.html#InExpression, InExpression) yields a pointer 594 to the value if the key is in the associative array, or null if not. 595 */ 596 final inout(T)* opBinaryRight(string op : "in")(in T item) inout nothrow pure @safe @nogc 597 { 598 return (item.key in this); 599 } 600 /// ditto 601 final inout(T)* opBinaryRight(string op : "in")(in key_type clIdx) inout nothrow pure @safe @nogc 602 { 603 return (clIdx in this._items); 604 } 605 /// ditto 606 final inout(T)* opBinaryRight(string op : "in", A...)(in A a) inout nothrow pure @safe @nogc 607 in 608 { 609 static assert(A.length == key_type.tupleof.length, T.stringof ~ 610 " has a clustered index with " ~ 611 key_type.tupleof.length.to!string ~ 612 " member(s). You included " ~ A.length.to!string ~ 613 " members when using 'in'."); 614 } 615 body 616 { 617 auto clIdx = key_type(a); 618 return (clIdx in this); 619 } 620 /** 621 Checks if the item has any conflicting unique constraints. This 622 is more extensive than $(SRCTAG KeyedCollection.contains). 623 624 $(B Precondition:) $(D_CODE assert(items !is null);) 625 626 $(B Postcondition:) 627 $(D_CODE 628 if (result) 629 assert(constraintName !is null && constraintName != ""); 630 else 631 assert(constraintName is null); 632 ) 633 */ 634 final bool violatesUniqueConstraints(in T item, out string constraintName) const nothrow pure 635 in 636 { 637 assert(item !is null, "Cannot check if a null item is duplicated."); 638 } 639 out (result) 640 { 641 if (result) 642 assert(constraintName !is null && constraintName != ""); 643 else 644 assert(constraintName is null); 645 } 646 body 647 { 648 bool result = false; 649 foreach(uniqueName; GetUniqueConstraintStructNames!(T)) 650 { 651 if (this._items.byValue.canFind!("a !is b && " ~ 652 "a." ~ uniqueName ~ "_key == " ~ 653 "b." ~ uniqueName ~ "_key")(item)) 654 { 655 result = true; 656 if (constraintName is null) 657 { 658 constraintName = uniqueName; 659 } 660 else 661 { 662 constraintName ~= ", " ~ uniqueName; 663 } 664 } 665 } 666 return result; 667 } 668 /// ditto 669 final bool violatesUniqueConstraints(in T item) const nothrow pure 670 { 671 string constraintName; 672 return this.violatesUniqueConstraints(item, constraintName); 673 } 674 675 /** 676 Checks if the item has any conflicting exclusion constraints. 677 678 $(B Precondition:) $(D_CODE assert(items !is null);) 679 680 $(B Postcondition:) 681 $(D_CODE 682 if (result) 683 assert(constraintName !is null && constraintName != ""); 684 else 685 assert(constraintName is null); 686 ) 687 */ 688 final bool violatesExclusionConstraints(in T item, out string constraintName) 689 in 690 { 691 assert(item !is null, "Cannot check if a null item is duplicated."); 692 } 693 out (result) 694 { 695 if (result) 696 assert(constraintName !is null && constraintName != ""); 697 else 698 assert(constraintName is null); 699 } 700 body 701 { 702 bool result = false; 703 static if (hasExclusionConstraints!T) 704 { 705 foreach(exc; GetExclusionConstraints!T) 706 { 707 foreach(i; this._items.byValue) 708 { 709 if (exc.exclusion(i, item)) 710 { 711 result = true; 712 if (constraintName is null) 713 { 714 constraintName = exc.name; 715 } 716 else 717 { 718 constraintName ~= ", " ~ exc.name; 719 } 720 break; 721 } 722 } 723 } 724 } 725 726 return result; 727 } 728 /// ditto 729 final bool violatesExclusionConstraints(in T item) 730 { 731 string constraintName; 732 return this.violatesExclusionConstraints(item, constraintName); 733 } 734 } 735 736 /// 737 unittest 738 { 739 // singular class this holds all of the columns 740 class Candy 741 { 742 private: 743 string _name; 744 int _ranking; 745 public: 746 // marking name as part of the primary key 747 @PrimaryKeyColumn @NotNull 748 @property string name() const nothrow pure @safe @nogc 749 { 750 return _name; 751 } 752 @property void name(string value) 753 { 754 setter(_name, value); 755 } 756 @property int ranking() const nothrow pure @safe @nogc 757 { 758 return _ranking; 759 } 760 // making sure that ranking will always be above 0 761 @CheckConstraint!(a => a > 0, "chk_Candys_ranking") 762 @property void ranking(int value) 763 { 764 setter(_ranking, value); 765 } 766 767 this(string name, int ranking) 768 { 769 this._name = name; 770 this._ranking = ranking; 771 // need to initialize the keyed item 772 initializeKeyedItem(); 773 } 774 Candy dup() const 775 { 776 return new Candy(this._name, this._ranking); 777 } 778 // the default is to make the primary key into the clustered index 779 // which allows you to search based on the primary key 780 mixin KeyedItem!(); 781 } 782 783 // plural class 784 // I am using an alias since BaseKeyedCollection 785 // takes care of everything I want to do for this example in one line. 786 alias Candies = BaseKeyedCollection!(Candy); 787 788 // Candies is a collection of Candy 789 static assert(is(Candies.collectionof == Candy)); 790 791 // source: 792 // http://www.bloomberg.com/ss/09/10/1021_americas_25_top_selling_candies/ 793 // should be Milky not Milkey, this is wrong on purpose 794 auto milkyWay = new Candy("Milkey Way", 18); 795 auto snickers = new Candy("Snickers", 4); 796 auto reesesPBCups = new Candy("Reese's Peanut Butter Cups", 2); 797 798 auto mars = new Candies([milkyWay, snickers]); 799 assert(mars.length == 2); 800 assert(!mars.containsChanges); 801 802 auto hershey = new Candies(reesesPBCups); 803 assert(hershey.length == 1); 804 805 // use the class as an index and confirm it returns the correct value 806 assert(mars[milkyWay] is milkyWay); 807 // use the primary key as an index 808 auto pk = Candy.PrimaryKey("Milkey Way"); 809 assert(pk.name == milkyWay.name); 810 assert(mars[pk] is milkyWay); 811 // use the contents of the primary key as an index 812 assert(mars["Milkey Way"] is milkyWay); 813 814 // milky way is in mars 815 assert(mars.contains(pk)); 816 // reesesPBCups is not in mars 817 assert(!mars.contains(reesesPBCups)); 818 819 // now we change the name to be correct 820 mars[pk].name = "Milky Way"; // remember pk is primary key for milky way 821 822 // since we changed milky way's name, mars contains changes 823 assert(mars.containsChanges); 824 825 // since we had name in pk spelled incorrectly 826 // and changed it, the primary key in mars has 827 // updated so Milkey Way is no longer in it but 828 // Milky Way is. 829 assert(!mars.contains("Milkey Way")); 830 assert(mars.contains("Milky Way")); 831 832 // looping over mars we make sure the key can be used to get 833 // the correct value. 834 foreach(name_pk, candy; mars) 835 { 836 assert(mars[name_pk] == candy); 837 } 838 839 // trying to add another candy with the same name will 840 // result in a unique constraint violation even if the ranking is different 841 auto milkyWay2 = new Candy("Milky Way", 16); 842 assert(milkyWay.name == milkyWay2.name); 843 assert(milkyWay.ranking != milkyWay2.ranking); 844 import std.exception : assertThrown; 845 assertThrown!(UniqueConstraintException)(mars ~= milkyWay2); 846 847 // ranking has a check constraint saying ranking always must be greater 848 // than 0. setting it to -1 resolves in a CheckConstraintException. 849 assertThrown!(CheckConstraintException)(mars["Milky Way"].ranking = -1); 850 // Since name is part of the primary key we must mark it with NotNull 851 // trying to set this to null will result in a CheckConstraintException. 852 assertThrown!(CheckConstraintException)(mars["Milky Way"].name = null); 853 854 // violatesUniqueConstraints will tell you which unique constraint 855 // is violated if any 856 string violatedConstraint; 857 assert(mars.violatesUniqueConstraints(milkyWay2, violatedConstraint)); 858 assert(violatedConstraint !is null && violatedConstraint == "PrimaryKey"); 859 860 // removing milky way from mars 861 mars.remove(milkyWay); 862 // this means milkyWay2 is no longer a duplicate 863 assert(!mars.violatesUniqueConstraints(milkyWay2, violatedConstraint)); 864 assert(violatedConstraint is null); 865 }